import { AppinsightsKey, isTestExecution, isUnitTestExecution, PVSC_EXTENSION_ID } from "../common/constants";

import { EventName } from "./constants";
import { LinterId } from "../linters/types";
import { StopWatch } from "../common/utils/stopWatch";
import TelemetryReporter from "@vscode/extension-telemetry";
import { isPromise } from "../common/utils/async";
import { EnvironmentType } from "../eqEnvironments/info";
import { TerminalShellType } from "../common/terminal/types";
import { DebugConfigurationType } from "../debugger/extension/types";

const sharedProperties: Record<string, unknown> = {};

export function setSharedProperty<P extends ISharedPropertyMapping, E extends keyof P>(name: E, value?: P[E]): void {
   const propertyName = name as string;
   // Ignore such shared telemetry during unit tests.
   if (isUnitTestExecution() && propertyName.startsWith('ds_')) {
      return;
   }
   if (value === undefined) {
      delete sharedProperties[propertyName];
   } else {
      sharedProperties[propertyName] = value;
   }
}

/**
 * Map all shared properties to their data types.
 */
export interface ISharedPropertyMapping {
   /**
    * For every DS telemetry we would like to know the type of Notebook Editor used when doing something.
    */
   ['ds_notebookeditor']: undefined | 'old' | 'custom' | 'native';

   /**
    * For every telemetry event from the extension we want to make sure we can associate it with install
    * source. We took this approach to work around very limiting query performance issues.
    */
   ['installSource']: undefined | 'marketPlace' | 'eqCodingPack';
}

function isTelemetrySupported(): boolean {
   try {
      const vsc = require('vscode');
      const reporter = require('@vscode/extension-telemetry');

      return vsc !== undefined && reporter !== undefined;
   } catch {
      return false;
   }
}

let telemetryReporter: TelemetryReporter | undefined;
function getTelemetryReporter() {
   if (!isTestExecution() && telemetryReporter) {
      return telemetryReporter;
   }
   const extensionId = PVSC_EXTENSION_ID;

   const { extensions } = require('vscode') as typeof import('vscode');
   const extension = extensions.getExtension(extensionId)!;
   const extensionVersion = extension.packageJSON.version;

   // eslint-disable-next-line @typescript-eslint/naming-convention
   const Reporter = require('@vscode/extension-telemetry').default as typeof TelemetryReporter;
   telemetryReporter = new Reporter(extensionId, extensionVersion, AppinsightsKey, true);

   return telemetryReporter;
}

export function sendTelemetryEvent<P extends IEventNamePropertyMapping, E extends keyof P>(
   eventName: E,
   measuresOrDurationMs?: Record<string, number> | number,
   properties?: P[E],
   ex?: Error,
): void {
   if (isTestExecution() || !isTelemetrySupported()) {
      return;
   }
   const reporter = getTelemetryReporter();
   const measures =
      typeof measuresOrDurationMs === 'number'
         ? { duration: measuresOrDurationMs }
         : measuresOrDurationMs || undefined;
   const customProperties: Record<string, string> = {};
   const eventNameSent = eventName as string;

   if (properties) {
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      const data = properties as any;
      Object.getOwnPropertyNames(data).forEach((prop) => {
         if (data[prop] === undefined || data[prop] === null) {
            return;
         }
         try {
            // If there are any errors in serializing one property, ignore that and move on.
            // Else nothing will be sent.
            switch (typeof data[prop]) {
               case 'string':
                  customProperties[prop] = data[prop];
                  break;
               case 'object':
                  customProperties[prop] = 'object';
                  break;
               default:
                  customProperties[prop] = data[prop].toString();
                  break;
            }
         } catch (exception) {
            console.error(`Failed to serialize ${prop} for ${eventName}`, exception);
         }
      });
   }

   // Add shared properties to telemetry props (we may overwrite existing ones).
   Object.assign(customProperties, sharedProperties);

   if (ex) {
      const errorProps = {
         errorName: ex.name,
         errorMessage: ex.message,
         errorStack: ex.stack ?? '',
      };
      Object.assign(customProperties, errorProps);

      // To avoid hardcoding the names and forgetting to update later.
      const errorPropNames = Object.getOwnPropertyNames(errorProps);
      reporter.sendTelemetryErrorEvent(eventNameSent, customProperties, measures, errorPropNames);
   } else {
      reporter.sendTelemetryEvent(eventNameSent, customProperties, measures);
   }

   if (process.env && process.env.VSC_EQ_LOG_TELEMETRY) {
      console.info(
         `Telemetry Event : ${eventNameSent} Measures: ${JSON.stringify(measures)} Props: ${JSON.stringify(
            customProperties,
         )} `,
      );
   }
}


export interface IEventNamePropertyMapping {
   [EventName.EQ_EXPERIMENTS_OPT_IN_OPT_OUT_SETTINGS]: {
      optedInto: string[];
      optedOutFrom: string[];
   };
   [EventName.EQ_EXPERIMENTS_INIT_PERFORMANCE]: unknown;

   [EventName.EDITOR_LOAD]: {
   };
   [EventName.EQ_INTERPRETER_DISCOVERY]: {
      interpreters?: number;
      environmentsWithoutEQ?: number;
      trigger: 'auto' | 'ui';
   };
   [EventName.ENVIRONMENT_WITHOUT_EQ_SELECTED]: never | undefined;
   [EventName.FORMAT_ON_TYPE]: {
      enabled: boolean;
   };
   [EventName.EXTENSION_SURVEY_PROMPT]: {
      selection: 'Yes' | 'Maybe later' | 'Do not show again' | undefined;
   };
   [EventName.TERMINAL_SHELL_IDENTIFICATION]: {
      failed: boolean;
      terminalProvided: boolean;
      shellIdentificationSource: 'terminalName' | 'settings' | 'environment' | 'default' | 'vscode';
      hasCustomShell: undefined | boolean;
      hasShellInEnv: undefined | boolean;
   };
   [EventName.SELECT_LINTER]: {
      tool?: LinterId;
      enabled: boolean;
   };
   [EventName.REPL]: never | undefined;
   [EventName.ACTIVATE_ENV_IN_CURRENT_TERMINAL]: {
      isTerminalVisible?: boolean;
   };
   [EventName.TERMINAL_CREATE]: {
      terminal?: TerminalShellType;
      triggeredBy?: 'commandpalette';
      pythonVersion?: string;
      interpreterType?: EnvironmentType;
   };
   [EventName.EXECUTION_CODE]: {
      scope: 'file' | 'selection';
      trigger?: 'command' | 'icon';
   };
   [EventName.EXECUTION_DJANGO]: {
      scope: 'file' | 'selection';
   };
   [EventName.DIAGNOSTICS_ACTION]: {
      commandName?: string;
      ignoreCode?: string;
      url?: string;
      action?: 'switchToCommandPrompt';
   };
   [EventName.DEBUGGER_CONFIGURATION_PROMPTS]: {
      configurationType: DebugConfigurationType;
      autoDetectedDjangoManagePyPath?: boolean;
      autoDetectedPyramidIniPath?: boolean;
      autoDetectedFastAPIMainPyPath?: boolean;
      autoDetectedFlaskAppPyPath?: boolean;
      manuallyEnteredAValue?: boolean;
   };
}

type TypedMethodDescriptor<T> = (
   target: unknown,
   propertyKey: string | symbol,
   descriptor: TypedPropertyDescriptor<T>,
) => TypedPropertyDescriptor<T> | void;

type FailedEventType = { failed: true };

export function captureTelemetry<This, P extends IEventNamePropertyMapping, E extends keyof P>(
   eventName: E,
   properties?: P[E],
   captureDuration = true,
   failureEventName?: E,
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   lazyProperties?: (obj: This, result?: any) => P[E],
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   lazyMeasures?: (obj: This, result?: any) => Record<string, number>,
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
): TypedMethodDescriptor<(this: This, ...args: any[]) => any> {
   return function (
      _target: unknown,
      _propertyKey: string | symbol,
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      descriptor: TypedPropertyDescriptor<(this: This, ...args: any[]) => any>,
   ) {
      const originalMethod = descriptor.value!;

      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      descriptor.value = function (this: This, ...args: any[]) {
         // Legacy case; fast path that sends event before method executes.
         // Does not set "failed" if the result is a Promise and throws an exception.
         if (!captureDuration && !lazyProperties && !lazyMeasures) {
            sendTelemetryEvent(eventName, undefined, properties);

            return originalMethod.apply(this, args);
         }

         // eslint-disable-next-line @typescript-eslint/no-explicit-any
         const getProps = (result?: any) => {
            if (lazyProperties) {
               return { ...properties, ...lazyProperties(this, result) };
            }
            return properties;
         };

         const stopWatch = captureDuration ? new StopWatch() : undefined;

         // eslint-disable-next-line @typescript-eslint/no-explicit-any
         const getMeasures = (result?: any) => {
            const measures = stopWatch ? { duration: stopWatch.elapsedTime } : undefined;
            if (lazyMeasures) {
               return { ...measures, ...lazyMeasures(this, result) };
            }
            return measures;
         };

         const result = originalMethod.apply(this, args);

         // If method being wrapped returns a promise then wait for it.
         if (result && isPromise(result)) {
            result
               .then((data) => {
                  sendTelemetryEvent(eventName, getMeasures(data), getProps(data));
                  return data;
               })
               .catch((ex) => {
                  const failedProps: P[E] = { ...getProps(), failed: true } as P[E] & FailedEventType;
                  sendTelemetryEvent(failureEventName || eventName, getMeasures(), failedProps, ex);
               });
         } else {
            sendTelemetryEvent(eventName, getMeasures(result), getProps(result));
         }

         return result;
      };

      return descriptor;
   };
}